package com.xsystem.api;

import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.sf.json.JSONException;
import net.sf.json.JSONObject;

import org.apache.commons.codec.binary.Base64OutputStream;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.ModelAndView;

import com.xsystem.api.exception.ActionNotSupportedException;
import com.xsystem.api.util.ApiMonitor;
import com.xsystem.api.util.ApiResponseHandler;
import com.xsystem.api.util.ApiResponseUtil;
import com.xsystem.api.util.IpRange;
import com.xsystem.util.StringUtil;

public abstract class XController extends ApiResponseHandler implements
		org.springframework.web.servlet.mvc.Controller, IController {

	public static final String ERROR_ACTION_NOT_SUPPORTED = "actionNotSupported";
	public static final String ERROR_IP_NOT_ALLOWED = "ipNotAllowed";
	public static final String ERROR_IP_INVALID = "ipInvalid";
	public static final String ERROR_RATE_LIMIT_REACHED = "rateLimitReached";
	public static final String ERROR_PARAMS_MISSING = "requiredParamsMissing";
	public static final String ERROR_IP_ADDRESS_REQUIRED = "ipAddressRequired";
	public static final String ERROR_INTERNAL_SERVER_ERROR = "internalServerError";
	public static final String HEADER_IP = "x-forwarded-for";

	private static final String CHARSET_UTF8 = "UTF-8";

	static final Logger log = LoggerFactory.getLogger(XController.class);

	protected ApiMonitor apiMonitor;

	public static enum ResponseType {
		NVP("nvp"), JSON("json"), XML("xml"), RAW("raw");

		private String name;

		private ResponseType(String name) {
			this.name = name;
		}

		public String getName() {
			return name;
		}

		@Override
		public String toString() {
			return name;
		}

		public boolean isEqual(String responseFormat) {
			return this.getName().equalsIgnoreCase(responseFormat);
		}

		private static final Map<String, ResponseType> lookup = new HashMap<String, ResponseType>();

		static {
			for (ResponseType c : ResponseType.values()) {
				lookup.put(c.getName(), c);
			}
		}

		public static ResponseType fromString(String code) {
			ResponseType responseType = lookup.get(code);
			if (responseType != null) {
				return responseType;
			} else {
				return ResponseType.NVP;
			}
		}
	}

	protected abstract String getDomain();

	protected String getActionCode(String uri) {
		// Trim "/public/" or "/private" from front
		String prefix = "/" + getDomain() + "/";

		int prefixIndex = uri.indexOf(prefix);
		if (prefixIndex == -1) {
			return ""; // not the intended domain: no action code
		}
		String trim = uri.substring(prefixIndex + prefix.length());

		// Trim "/nvp" or "/json" or "/xml" from the back.
		int i = trim.lastIndexOf('/');
		if (i == -1) {
			return "";
		}
		trim = trim.substring(0, i);

		// Change the slashes to dots.
		String action = trim.replace('/', '.');
		return action;
	}

	// Implemented by subclass to identify the bean name.
	protected abstract String getBeanName();

	protected abstract String doAction(HttpServletRequest request,
			ApplicationContext context);

	protected String getRequestIp(HttpServletRequest request) {
		String headerIp = request.getHeader(HEADER_IP);
		if (StringUtil.isEmpty(headerIp)) {
			headerIp = request.getRemoteAddr();
		}
		return headerIp;
	}

	@Override
	@Transactional
	public void executeTransactionally(HttpServletRequest request,
			HttpServletResponse response, ApplicationContext context) {
		apiMonitor.setUri(request.getRequestURI());
		apiMonitor.setRequestIp(this.getRequestIp(request));

		String output = doAction(request, context);
		try {
			response.setContentType("text/plain; charset=UTF-8");
			response.setCharacterEncoding(CHARSET_UTF8); // ensure the writer is
															// in UTF-8
			response.getWriter().write(output);
		} catch (IOException ex) {
			apiMonitor.appendMessage(ex.getMessage());
		}
	}

	@Override
	public ModelAndView handleRequest(HttpServletRequest request,
			HttpServletResponse response) {

		try {
			request.setCharacterEncoding(CHARSET_UTF8); // We want to process
														// the parameters as
														// UTF-8
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}

		apiMonitor.begin();
		ApplicationContext context = WebApplicationContextUtils
				.getWebApplicationContext(request.getSession()
						.getServletContext());
		IController c = (IController) context.getBean(getBeanName()); // somehow
																		// must
																		// use
																		// interface
		try {
			c.executeTransactionally(request, response, context);
		} catch (RuntimeException e) { // catch any unhandled runtime exception
										// and report to noc
			log.error(e.getMessage(), e);
			apiMonitor.appendDeveloperException(e, true);
			ResponseType responseFormat = getResponseFormat(request
					.getRequestURI());
			Map<String, Object> map = prepareErrorResponse(
					ERROR_INTERNAL_SERVER_ERROR, null);
			String responseString = formatResponse(map, responseFormat);
			try {
				response.getWriter().append(responseString);
			} catch (IOException e1) {
				log.error(e1.getMessage(), e1);
				apiMonitor.appendMessage(e1.getMessage());
			}
		}
		apiMonitor.record();
		return null;
	}

	protected Map<String, String> getParameters(HttpServletRequest request) {
		HashMap<String, String> parameters = new HashMap<String, String>();

		Enumeration<String> enumeration = request.getParameterNames();
		while (enumeration.hasMoreElements()) {
			String paramName = enumeration.nextElement();
			parameters.put(paramName, request.getParameter(paramName));
		}

		String body = getBody(request).trim();
		if (body.startsWith("{") && body.endsWith("}")) {
			parameters.putAll(getJSONRequestParametersFromBody(body));
		} else {
			if (!ServletFileUpload.isMultipartContent(request)) {
				parameters.putAll(getRequestParametersFromBody(body));
			}
		}
		return parameters;
	}

	@SuppressWarnings("unchecked")
	protected HashMap<String, String> getMultiPartContent(
			HttpServletRequest request) {
		HashMap<String, String> parameters = new HashMap<String, String>();
		try {
			JSONObject files = new JSONObject();
			FileItemFactory factory = new DiskFileItemFactory();
			ServletFileUpload upload = new ServletFileUpload(factory);
			List<FileItem> items = upload.parseRequest(request);
			for (FileItem item : items) {

				if (item.isFormField()) {
					parameters.put(item.getFieldName(), item.getString());
				} else {
					if (item.get().length > 0) {
						BufferedImage image = ImageIO.read(item
								.getInputStream());
						ByteArrayOutputStream os = new ByteArrayOutputStream();
						OutputStream b64 = new Base64OutputStream(os);
						ImageIO.write(image, "png", b64);
						files.put(item.getFieldName(), os.toString());
						os.close();

					}
				}
			}

			if (files.size() > 0) {
				parameters.put(Api.IN_UPLOADS, files.toString());
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return parameters;
	}

	private String getBody(HttpServletRequest request) {
		StringBuilder sb = new StringBuilder();
		BufferedReader input = null;
		try {
			input = new BufferedReader(new InputStreamReader(
					request.getInputStream()));

			sb = new StringBuilder();
			String str;
			while (null != ((str = input.readLine()))) {
				sb.append(str);
			}

		} catch (Exception e) {
			e.printStackTrace();
			log.error(e.getMessage(), e);
		} finally {
			if (input != null) {
				try {
					input.close();
				} catch (IOException ex) {
					apiMonitor.appendMessage(ex.getMessage());
				}
			}
		}
		return sb.toString();
	}

	@SuppressWarnings("rawtypes")
	private Map<String, String> getJSONRequestParametersFromBody(String body) {
		Map<String, String> parameters = new HashMap<String, String>();
		try {
			JSONObject obj = JSONObject.fromObject(body);
			Iterator iterator = obj.keys();
			while (iterator.hasNext()) {
				String key = iterator.next().toString();
				parameters.put(key, obj.get(key).toString());
			}
		} catch (JSONException ex) {
			ex.printStackTrace();

			// do nothing
		}
		return parameters;
	}

	private Map<String, String> getRequestParametersFromBody(String body) {
		Map<String, String> map = new HashMap<String, String>();
		map.putAll(this.parseNvp(body, "&"));
		return map;
	}

	private Map<String, String> parseNvp(final String inputString,
			final String delim) {
		if (inputString == null || inputString.trim().equals("")) {
			return new HashMap<String, String>();
		}

		final HashMap<String, String> hm = new HashMap<String, String>();

		final StringTokenizer st = new StringTokenizer(inputString, delim);

		while (st.hasMoreTokens()) {
			final String s = st.nextToken();
			final String[] nvp = s.split("=", 2);
			try {
				hm.put(nvp[0],
						URLDecoder.decode(String.valueOf(nvp[1]), CHARSET_UTF8));
			} catch (UnsupportedEncodingException ex) {
				throw new RuntimeException(ex);
			}
		}
		return hm;
	}

	protected ResponseType getResponseFormat(String uri) {
		String responseFormat = uri.substring(uri.lastIndexOf("/") + 1);
		return ResponseType.fromString(responseFormat);
	}

	protected Map<String, Object> executeAction(Map<String, String> params,
			String action, ApplicationContext context)
			throws ActionNotSupportedException {
		Api api = null;
		try {
			api = (Api) context.getBean(action);
		} catch (BeansException e) {
			throw new ActionNotSupportedException();
		}

		return api.execute(params);

	}

	protected String formatResponse(Map<String, Object> response,
			ResponseType responseFormat) {
		if (ResponseType.JSON.equals(responseFormat)) {
			return ApiResponseUtil.prepareResponseJson(response);
		} else if (ResponseType.XML.equals(responseFormat)) {
			return ApiResponseUtil.prepareResponseXml(response);
		} else {
			return ApiResponseUtil.prepareResponseNameValuePair(response);
		}
	}

	protected boolean isIpAllowed(String ipsAllowedStr, String ipAddress) {

		// Disallow if null or empty
		if (ipsAllowedStr == null || ipsAllowedStr.isEmpty()) {
			return false;
		}

		// Allow if "*"
		String ipsAllowed = ipsAllowedStr.trim();
		if (ipsAllowed.equals("*")) {
			return true;
		}

		// Single IP eg: 1.1.1.1, 1::FFFE:1234
		// Range IPs eg: 1.1.1.1-1.1.2.222, 1:FFFE:1-1:FFFE:FFFF
		// Combination eg: 1.1.1.1, 2.2.2.2, 2.1.1.1-2.1.3.255, 1::FFFE:1234
		String[] ipsAllowedArray = ipsAllowed.split("[,;]");
		for (String ipAllowed : ipsAllowedArray) {

			try {
				// Prepare the IP range specified by "ipAllowed"
				IpRange compareIpRange = null;
				if (ipAllowed.contains("-")) {
					String[] range = ipAllowed.split("-");
					if (range.length != 2) {
						continue;
					}
					compareIpRange = new IpRange(range[0].trim(),
							range[1].trim());
				} else {
					compareIpRange = new IpRange(ipAllowed.trim());
				}

				// Check if IP is within range
				if (compareIpRange.isIpWithinRange(ipAddress.trim())) {
					return true;
				}
			} catch (UnknownHostException e) {
				log.error("IP format error: " + ipAllowed);
			}
		}
		return false;
	}

	public ApiMonitor getApiMonitor() {
		return apiMonitor;
	}

	public void setApiMonitor(ApiMonitor apiMonitor) {
		this.apiMonitor = apiMonitor;
	}

}
